<template>
  <div class="jianpu-scroll-container jianpu-editor-svg" tabindex="0" @keydown="onKeyDown" style="position:relative;" @click="clearSelectedMeasure">
    <svg :width="1030" :height="svgHeight" @mousemove="onDragMove" @mouseup="onDragEnd" @mouseleave="onDragEnd">
      <g v-for="(row, rowIdx) in rows" :key="rowIdx" :transform="`translate(0, ${topPadding + rowIdx * rowHeight})`">
        <g v-for="(measure, mIdx) in row" :key="measure.index"
           :class="['measure-group', isSelectedMeasure(rowIdx, mIdx) ? 'selected-measure' : '']"
           @click.stop="selectMeasure(rowIdx, mIdx)"
           :transform="`translate(${mIdx * measureWidth + leftPadding}, 0)`"
           @contextmenu.stop="onContextMenu($event, rowIdx, mIdx)">
          <!-- 点击热区，确保整个小节都可高亮 -->
          <rect
            v-if="!draggingActive"
            :x="0"
            :y="0"
            :width="measureWidth"
            :height="isDualStaff ? staffSpacing * 2 + 36 : staffSpacing + 36"
            fill="transparent"
            style="pointer-events:all"
          />
          <!-- 花括弧和手型提示：每行第一个小节左侧显示 -->
          <g v-if="mIdx === 0">
            <text v-if="isDualStaff" :x="-leftPadding + 18" :y="noteBaseY + staffSpacing/2 + 11" font-size="120" fill="#222" font-family="serif" dominant-baseline="middle">{</text>
            <text :x="10" :y="noteBaseY + 2" font-size="15" fill="#444" font-family="serif">右手</text>
            <text v-if="isDualStaff" :x="10" :y="noteBaseY + staffSpacing + 2" font-size="15" fill="#444" font-family="serif">左手</text>
          </g>
          <!-- 小节编号 -->
          <text :x="numberZone/2" :y="noteBaseY - 24" font-size="13" fill="#888" font-family="serif">({{ measure.index }})</text>
          <!-- 右手音符格（独立占位单元） -->
          <g v-for="i in noteCount" :key="'r'+i">
            <g class="cell" :transform="`translate(${numberZone + (i-1) * noteSpacing + 4}, ${noteBaseY - 18})`" @click.stop="(e) => { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; setCursor(rowIdx, mIdx, 0, i-1, x); }" style="cursor:pointer;">
              <rect width="24" height="30" rx="3" fill="#f8faff"
                :stroke="showHighlight && !isAnyLyricEditing && isCursor(rowIdx, mIdx, 0, i-1) ? (getCell(measure,0,i-1).isEighth ? (cursor.eighthPart==='main' ? '#409eff' : (cursor.eighthPart==='sub' ? '#67c23a' : '#409eff')) : '#409eff') : (isPlayingCell(rowIdx, mIdx, 0, i-1) ? '#ffd600' : 'none')"
                stroke-width="2" />
              <!-- 上点 -->
              <g v-if="getCell(measure,0,i-1).dotUp === 4">
                <text x="12" y="-17" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="-9" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="-1" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,0,i-1).dotUp === 3">
                <text x="12" y="-7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="0" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,0,i-1).dotUp === 2">
                <text x="12" y="-3" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="5" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <text v-else-if="getCell(measure,0,i-1).dotUp === 1" x="12" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              <!-- 主音符 -->
              <text :x="getCell(measure,0,i-1).isEighth ? 8 : 12" y="18" font-size="22" fill="#222" text-anchor="middle" alignment-baseline="middle" font-family="serif">{{ getCell(measure,0,i-1).note }}</text>
              <!-- 八分音符的次音符（x=20） -->
              <text v-if="getCell(measure,0,i-1).isEighth" x="20" y="18" font-size="22" fill="#222" text-anchor="middle" alignment-baseline="middle" font-family="serif">{{ getCell(measure,0,i-1).subNote }}</text>
              <!-- 主音符的上点 -->
              <g v-if="getCell(measure,0,i-1).dotUp === 4">
                <text x="12" y="-17" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="-9" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="-1" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,0,i-1).dotUp === 3">
                <text x="12" y="-7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="0" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,0,i-1).dotUp === 2">
                <text x="12" y="-3" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="5" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <text v-else-if="getCell(measure,0,i-1).dotUp === 1" x="12" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              <!-- 主音符的下点 -->
              <g v-if="getCell(measure,0,i-1).dotDown === 4">
                <text x="12" y="29" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="35" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="41" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="47" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,0,i-1).dotDown === 3">
                <text x="12" y="29" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="35" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="41" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,0,i-1).dotDown === 2">
                <text x="12" y="29" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="35" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,0,i-1).dotDown === 1">
                <text x="12" y="29" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <!-- 八分音符的次音符 -->
              <text v-if="getCell(measure,0,i-1).isEighth" x="20" y="18" font-size="22" fill="#222" text-anchor="middle" alignment-baseline="middle" font-family="serif">{{ getCell(measure,0,i-1).subNote }}</text>
              <!-- 次音符的上点 -->
              <g v-if="getCell(measure,0,i-1).isEighth && getCell(measure,0,i-1).subDotUp === 4">
                <text x="20" y="-17" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="20" y="-9" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="20" y="-1" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="20" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,0,i-1).isEighth && getCell(measure,0,i-1).subDotUp === 3">
                <text x="20" y="-7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="20" y="0" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="20" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,0,i-1).isEighth && getCell(measure,0,i-1).subDotUp === 2">
                <text x="20" y="-3" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="20" y="5" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <text v-else-if="getCell(measure,0,i-1).isEighth && getCell(measure,0,i-1).subDotUp === 1" x="20" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              <!-- 次音符的下点 -->
              <g v-if="getCell(measure,0,i-1).isEighth && getCell(measure,0,i-1).subDotDown === 4">
                <text x="20" y="29" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="20" y="35" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="20" y="41" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="20" y="47" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,0,i-1).isEighth && getCell(measure,0,i-1).subDotDown === 3">
                <text x="20" y="29" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="20" y="35" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="20" y="41" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,0,i-1).isEighth && getCell(measure,0,i-1).subDotDown === 2">
                <text x="20" y="29" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="20" y="35" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,0,i-1).isEighth && getCell(measure,0,i-1).subDotDown === 1">
                <text x="20" y="29" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <!-- 横线/延音线 -->
              <rect v-if="getCell(measure,0,i-1).line" x="4" y="26" width="16" height="2" fill="#222" rx="1" />
              <!-- 八分音符的下划线 -->
              <rect v-if="getCell(measure,0,i-1).isEighth" x="2" y="26" width="24" height="2" fill="#222" rx="1" />
              <!-- 和弦名 -->
              <text
                v-if="getCell(measure,0,i-1).chord"
                x="12"
                :y="getChordY(getCell(measure,0,i-1).dotUp) - 8"
                font-size="15"
                fill="#222"
                text-anchor="middle"
                alignment-baseline="middle"
              >
                {{ getCell(measure,0,i-1).chord }}
              </text>
              <!-- 八分音符高亮：主音/次音分别半格高亮（底层） -->
              <rect v-if="getCell(measure,0,i-1).isEighth && isCursor(rowIdx, mIdx, 0, i-1) && cursor.eighthPart==='main'" x="1" y="1" width="10" height="28" fill="rgba(64,158,255,0.08)" stroke="#409eff" stroke-width="2" rx="2" />
              <rect v-if="getCell(measure,0,i-1).isEighth && isCursor(rowIdx, mIdx, 0, i-1) && cursor.eighthPart==='sub'" x="13" y="1" width="10" height="28" fill="rgba(103,194,58,0.08)" stroke="#67c23a" stroke-width="2" rx="2" />
              <!-- 普通音符整体高亮 -->
              <rect v-else-if="isCursor(rowIdx, mIdx, 0, i-1)" width="24" height="30" rx="3" fill="none" stroke="#409eff" stroke-width="2" />
            </g>
          </g>
          <!-- 左手音符格（独立占位单元） -->
          <g v-if="isDualStaff" v-for="i in noteCount" :key="'l'+i">
            <g class="cell" :transform="`translate(${numberZone + (i-1) * noteSpacing + 4}, ${noteBaseY - 18 + staffSpacing})`" @click.stop="(e) => { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; setCursor(rowIdx, mIdx, 1, i-1, x); }" style="cursor:pointer;">
              <rect width="24" height="30" rx="3" fill="#f8faff"
                :stroke="showHighlight && !isAnyLyricEditing && isCursor(rowIdx, mIdx, 1, i-1) ? (getCell(measure,1,i-1).isEighth ? (cursor.eighthPart==='main' ? '#409eff' : (cursor.eighthPart==='sub' ? '#67c23a' : '#409eff')) : '#409eff') : (isPlayingCell(rowIdx, mIdx, 1, i-1) ? '#ffd600' : 'none')"
                stroke-width="2" />
              <!-- 上点 -->
              <g v-if="getCell(measure,1,i-1).dotUp === 4">
                <text x="12" y="-17" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="-9" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="-1" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,1,i-1).dotUp === 3">
                <text x="12" y="-7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="0" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,1,i-1).dotUp === 2">
                <text x="12" y="-3" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="5" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <text v-else-if="getCell(measure,1,i-1).dotUp === 1" x="12" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              <!-- 主音符 -->
              <text :x="getCell(measure,1,i-1).isEighth ? 8 : 12" y="18" font-size="22" fill="#222" text-anchor="middle" alignment-baseline="middle" font-family="serif">{{ getCell(measure,1,i-1).note }}</text>
              <!-- 八分音符的次音符（x=20） -->
              <text v-if="getCell(measure,1,i-1).isEighth" x="20" y="18" font-size="22" fill="#222" text-anchor="middle" alignment-baseline="middle" font-family="serif">{{ getCell(measure,1,i-1).subNote }}</text>
              <!-- 主音符的上点 -->
              <g v-if="getCell(measure,1,i-1).dotUp === 4">
                <text x="12" y="-17" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="-9" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="-1" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,1,i-1).dotUp === 3">
                <text x="12" y="-7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="0" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,1,i-1).dotUp === 2">
                <text x="12" y="-3" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="5" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <text v-else-if="getCell(measure,1,i-1).dotUp === 1" x="12" y="7" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              <!-- 主音符的下点 -->
              <g v-if="getCell(measure,1,i-1).dotDown === 4">
                <text x="12" y="29" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="35" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="41" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="47" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,1,i-1).dotDown === 3">
                <text x="12" y="29" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="35" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="41" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <g v-else-if="getCell(measure,1,i-1).dotDown === 2">
                <text x="12" y="29" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
                <text x="12" y="35" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              </g>
              <text v-else-if="getCell(measure,1,i-1).dotDown === 1" x="12" y="29" font-size="13" fill="#222" text-anchor="middle" alignment-baseline="middle">•</text>
              <!-- 横线/延音线 -->
              <rect v-if="getCell(measure,1,i-1).line" x="4" y="26" width="16" height="2" fill="#222" rx="1" />
              <!-- 八分音符的下划线 -->
              <rect v-if="getCell(measure,1,i-1).isEighth" x="2" y="26" width="24" height="2" fill="#222" rx="1" />
              <!-- 和弦名 -->
              <text
                v-if="getCell(measure,1,i-1).chord"
                x="12"
                :y="getChordY(getCell(measure,1,i-1).dotUp)"
                font-size="15"
                fill="#222"
                text-anchor="middle"
                alignment-baseline="middle"
              >
                {{ getCell(measure,1,i-1).chord }}
              </text>
              <!-- 八分音符高亮：主音/次音分别半格高亮（左手，底层） -->
              <rect v-if="getCell(measure,1,i-1).isEighth && isCursor(rowIdx, mIdx, 1, i-1) && cursor.eighthPart==='main'" x="1" y="1" width="10" height="28" fill="rgba(64,158,255,0.08)" stroke="#409eff" stroke-width="2" rx="2" />
              <rect v-if="getCell(measure,1,i-1).isEighth && isCursor(rowIdx, mIdx, 1, i-1) && cursor.eighthPart==='sub'" x="13" y="1" width="10" height="28" fill="rgba(103,194,58,0.08)" stroke="#67c23a" stroke-width="2" rx="2" />
              <!-- 普通音符整体高亮（左手） -->
              <rect v-else-if="isCursor(rowIdx, mIdx, 1, i-1)" width="24" height="30" rx="3" fill="none" stroke="#409eff" stroke-width="2" />
            </g>
          </g>
          <!-- 小节线（右） -->
          <g v-if="!isLastMeasure(rowIdx, mIdx)" class="barline" @click="onBarlineClick('bar', rowIdx, mIdx)" style="cursor:pointer;">
            <rect :x="measureWidth - barZone/2 - 1" :y="noteBaseY - 18" width="2" :height="(isDualStaff ? staffSpacing * 2 : staffSpacing) + 6" fill="#222" />
          </g>
          <!-- 终止线：最后一个小节右侧画两竖线（左细右粗，间距更近） -->
          <g v-if="isLastMeasure(rowIdx, mIdx)" class="barline" @click="onBarlineClick('end', rowIdx, mIdx)" style="cursor:pointer;">
            <rect :x="measureWidth - 8" :y="noteBaseY - 18" width="2" :height="(isDualStaff ? staffSpacing * 2 : staffSpacing) + 6" fill="#222" />
            <rect :x="measureWidth - 4" :y="noteBaseY - 18" width="4" :height="(isDualStaff ? staffSpacing * 2 : staffSpacing) + 6" fill="#222" />
          </g>
          <!-- 左右手间距拖动手柄（仅双声部） -->
          <rect v-if="isDualStaff" :x="numberZone" :y="noteBaseY - 18 + staffSpacing + 30 + 8" :width="cellZone" height="16" fill="rgba(64,160,255,0.08)" cursor="row-resize" @mousedown="startStaffDrag($event)" />
          <!-- 歌词显示/编辑区域 -->
          <g>
            <foreignObject
              :x="numberZone"
              :y="getLyricY()"
              :width="cellZone"
              height="32"
              style="overflow:visible;"
            >
              <template v-if="measure.lyricEditing">
                <input
                  ref="lyricInput"
                  v-model="measure.lyricDraft"
                  @blur="onLyricInputBlur(measure, rowIdx, mIdx)"
                  @keyup.enter="onLyricInputEnter(measure, rowIdx, mIdx)"
                  class="jianpu-lyric-inline-input"
                  style="width:100%;text-align:left;font-size:12px;color:#ff9800;border:1.5px solid #ff9800;border-radius:4px;padding:2px 6px;outline:none;background:#fff7e6;"
                  :placeholder="'请输入歌词'"
                  autofocus
                />
              </template>
              <template v-else>
                <div
                  v-if="measure.lyric"
                  class="jianpu-lyric-inline-text"
                  style="width:100%;text-align:left;font-size:12px;color:#ff9800;user-select:none;cursor:pointer;"
                  @click="onLyricTextClick(measure)"
                >{{ measure.lyric }}</div>
              </template>
            </foreignObject>
          </g>
          <!-- 高亮框用g包裹，pointer-events:none，放在最后 -->
          <g v-if="isSelectedMeasure(rowIdx, mIdx) && !isAnyLyricEditing && enableEdit" style="pointer-events:none">
            <rect
              :x="0" :y="0"
              :width="measureWidth - barZone"
              :height="isDualStaff ? staffSpacing * 2 + 36 : staffSpacing + 36"
              fill="none" stroke="#ff9800" stroke-width="3" rx="8"
            />
          </g>
        </g>
        <!-- 行间距拖动手柄 -->
        <rect :x="0" :y="rowHeight-handleHeight" :width="svgWidth" :height="handleHeight" fill="rgba(64,160,255,0.06)" cursor="row-resize" style="pointer-events:auto;" @mousedown="startRowDrag(rowIdx, $event)" />
      </g>
    </svg>
    <div v-if="showLoading" class="jianpu-loading-mask">
      <div class="jianpu-loading-spinner"></div>
      <div class="jianpu-loading-text">正在加载钢琴音色，请稍候...</div>
    </div>
    <div v-if="showContextMenu" class="jianpu-context-menu" :style="{left: contextMenuX + 'px', top: contextMenuY + 'px'}" @mousedown.stop>
      <div class="jianpu-context-menu-item" @click="onDeleteMeasureMenu">删除当前小节</div>
      <div class="jianpu-context-menu-item" @click="onAddLyricMenu">添加歌词</div>
      <div class="jianpu-context-menu-item" @click="onInsertMeasureMenu">插入小节</div>
    </div>
  </div>
</template>
<script>
import { mapGetters } from 'vuex'
import * as Tone from 'tone'
export default {
  name: 'JianpuEditor',
  props: {
    currentSymbol: {
      type: Object,
      default: () => ({ duration: '四分音符' })
    },
    currentChord: {
      type: String,
      default: null
    },
    enableKeyboardPlayback: {
      type: Boolean,
      default: false
    },
    enableEdit: {
      type: Boolean,
      default: false
    },
    showHighlight: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      measuresPerRow: 4,
      rowHeight: (this.scoreDraft && this.scoreDraft.scoreType === '双声部') ? 100*2+52 : 100+52,
      numberZone: 40, // 编号区宽度
      barZone: 16, // 小节线区宽度
      staffSpacing: 100,
      noteBaseY: 44,
      leftPadding: 70,
      rightPadding: 120,
      topPadding: 32, // svg顶部留白，防止和弦名被遮挡
      localMeasures: [], // 初始化在mounted里
      cursor: { rowIdx: 0, mIdx: 0, staffIdx: 0, noteIdx: 0, eighthPart: null },
      dragging: null, // {type: 'staff'|'row', startY, startVal, rowIdx}
      draggingActive: false, // 鼠标按下且正在拖动手柄
      contentHeight: 140,
      handleHeight: 16,
      handleGap: 8,
      playingCursor: null,
      pianoLoaded: false,
      piano: null,
      isPlaying: false,
      stopRequested: false,
      undoStack: [],
      redoStack: [],
      maxUndoStackSize: 50, // 限制撤销栈大小
      showLoading: false,
      selectedMeasure: null,
      showContextMenu: false, // 右键菜单显示
      contextMenuX: 0,
      contextMenuY: 0,
      // 拖拽限制
      minStaffSpacing: 80,
      maxStaffSpacing: 200,
      minRowHeight: 100,
      maxRowHeight: 300,
      isUndoRedoOperation: false, // 添加标记，防止撤销/重做操作触发新的撤销记录
      eighthEditTarget: null, // 'main' | 'sub' | null
    }
  },
  computed: {
    ...mapGetters(['scoreDraft']),
    isDualStaff() {
      return this.scoreDraft.scoreType === '双声部';
    },
    noteCount() {
      // 支持4/4, 3/4, 2/4, 6/8
      const beat = this.scoreDraft?.beat || '4/4';
      if (beat === '4/4') return 4;
      if (beat === '3/4') return 3;
      if (beat === '2/4') return 2;
      if (beat === '6/8') return 6;
      return 4;
    },
    noteSpacing() {
      // 动态格间距，格子多则更紧凑
      if (this.noteCount >= 6) return 28;
      if (this.noteCount === 2) return 60;
      if (this.noteCount === 3) return 48;
      return 40; // 4/4
    },
    cellZone() {
      return this.noteSpacing * this.noteCount + 16; // 16为padding
    },
    measureWidth() {
      return this.numberZone + this.cellZone + this.barZone;
    },
    rows() {
      const res = [];
      for (let i = 0; i < this.localMeasures.length; i += this.measuresPerRow) {
        res.push(this.localMeasures.slice(i, i + this.measuresPerRow));
      }
      return res;
    },
    svgWidth() {
      return this.leftPadding + this.measuresPerRow * this.measureWidth + this.rightPadding;
    },
    svgHeight() {
      return this.topPadding + this.rows.length * this.rowHeight + 40;
    },
    isAnyLyricEditing() {
      // 检查所有小节是否有lyricEditing为true
      return this.localMeasures.some(m => m.lyricEditing);
    }
  },
  methods: {
    getCell(measure, staffIdx, noteIdx) {
      return measure.notes?.[staffIdx]?.[noteIdx] || {note: '', dotUp: false, dotDown: false, line: false};
    },
    getChordY(dotUp) {
      // dotUp: 0~4
      if (dotUp === 4) return -32;
      if (dotUp === 3) return -25;
      if (dotUp === 2) return -18;
      if (dotUp === 1) return -11;
      return -7;
    },
    applyChordToCurrentCell(chord) {
      const c = this.cursor;
      const measure = this.rows[c.rowIdx]?.[c.mIdx];
      if (!measure) return;
      const cell = this.getCell(measure, c.staffIdx, c.noteIdx);
      if (cell) {
        this.$set(cell, 'chord', chord);
      }
    },
    isCursor(rowIdx, mIdx, staffIdx, noteIdx) {
      const c = this.cursor;
      return c.rowIdx === rowIdx && c.mIdx === mIdx && c.staffIdx === staffIdx && c.noteIdx === noteIdx;
    },
    setCursor(rowIdx, mIdx, staffIdx, noteIdx, clickX) {
      let eighthPart = null;
      const measure = this.rows[rowIdx]?.[mIdx];
      if (measure) {
        const cell = this.getCell(measure, staffIdx, noteIdx);
        if (cell && cell.isEighth && cell.note && cell.subNote) {
          eighthPart = (clickX < 12) ? 'main' : 'sub';
          this.eighthEditTarget = eighthPart;
        } else {
          this.eighthEditTarget = null;
        }
      } else {
        this.eighthEditTarget = null;
      }
      this.cursor = { rowIdx, mIdx, staffIdx, noteIdx, clickX, eighthPart };
      this.selectedMeasure = null;
    },
    isLastMeasure(rowIdx, mIdx) {
      const flatIdx = rowIdx * this.measuresPerRow + mIdx;
      return flatIdx === this.localMeasures.length - 1;
    },
    onKeyDown(e) {
      if (!this.enableEdit) return;
      if (this.isAnyLyricEditing) return;
      if (!this.isUndoRedoOperation) {
        this.pushUndo();
      }
      console.log('onKeyDown', e.key, JSON.parse(JSON.stringify(this.currentSymbol)));
      const c = this.cursor;
      const measure = this.rows[c.rowIdx]?.[c.mIdx];
      if (!measure) return;
      
      // 确保notes数组已初始化
      if (!measure.notes) {
        this.$set(measure, 'notes', Array.from({ length: this.isDualStaff ? 2 : 1 }, () => []));
      }
      if (!measure.notes[c.staffIdx]) {
        this.$set(measure.notes, c.staffIdx, []);
      }
      
      const cell = this.getCell(measure, c.staffIdx, c.noteIdx);
      // 删除小节
      if ((e.key === 'Delete' || e.key === 'Backspace') && this.selectedMeasure) {
        e.preventDefault();
        this.confirmDeleteMeasure();
        return;
      }
      // 只允许数字1-7
      if (/^[1-7]$/.test(e.key)) {
        // 获取当前符号时值
        const duration = this.currentSymbol?.duration;
        const isWhole = duration === '全音符' || duration === 1;
        const isHalf = duration === '二分音符' || duration === 0.5;
        const isThird = duration === '三分音符' || duration === 0.333;
        const isEighth = duration === '八分音符' || duration === 0.125;
        let dotUp = 0;
        let dotDown = 0;
        let octave = 4;
        if (this.currentSymbol?.pitch === '小字五组') { dotUp = 4; octave = 8; }
        else if (this.currentSymbol?.pitch === '小字四组') { dotUp = 3; octave = 7; }
        else if (this.currentSymbol?.pitch === '小字三组') { dotUp = 2; octave = 6; }
        else if (this.currentSymbol?.pitch === '小字二组') { dotUp = 1; octave = 5; }
        else if (this.currentSymbol?.pitch === '小字组') { dotDown = 1; octave = 3; }
        else if (this.currentSymbol?.pitch === '大字组') { dotDown = 2; octave = 2; }
        else if (this.currentSymbol?.pitch === '大字一组') { dotDown = 3; octave = 1; }
        else if (this.currentSymbol?.pitch === '大字二组') { dotDown = 4; octave = 0; }
        
        // 使用Vue的响应式方法更新状态
        if (isEighth) {
          // 如果是八分音符，检查当前格子是否已经有主音符
          if (!cell.note) {
            // 如果没有主音符，添加主音符
            this.$set(cell, 'note', e.key);
            this.$set(cell, 'duration', this.getDurationValue(duration));
            this.$set(cell, 'isEighth', true);
            this.$set(cell, 'dotUp', dotUp);
            this.$set(cell, 'dotDown', dotDown);
            this.$set(cell, 'pitch', this.currentSymbol?.pitch);
            this.$set(cell, 'pitchIndex', this.currentSymbol?.pitchIndex);
            this.$set(cell, 'octave', octave);
            // 光标保持在当前位置，等待输入次音符
          } else if (!cell.subNote) {
            // 如果有主音符但没有次音符，添加次音符
            this.$set(cell, 'subNote', e.key);
            this.$set(cell, 'subDotUp', dotUp);
            this.$set(cell, 'subDotDown', dotDown);
            this.$set(cell, 'subPitch', this.currentSymbol?.pitch);
            this.$set(cell, 'subPitchIndex', this.currentSymbol?.pitchIndex);
            this.$set(cell, 'subOctave', octave);
            // 移动光标到下一个格子
            if (c.noteIdx + 1 < this.noteCount) {
              this.setCursor(c.rowIdx, c.mIdx, c.staffIdx, c.noteIdx + 1);
            } else if (c.mIdx < this.measuresPerRow - 1) {
              this.setCursor(c.rowIdx, c.mIdx + 1, c.staffIdx, 0);
            } else if (c.rowIdx < this.rows.length - 1) {
              this.setCursor(c.rowIdx + 1, 0, c.staffIdx, 0);
            }
          } else {
            // 优先用cursor.eighthPart判断
            if (this.cursor.eighthPart === 'main') {
              this.$set(cell, 'note', e.key);
              this.$set(cell, 'dotUp', dotUp);
              this.$set(cell, 'dotDown', dotDown);
              this.$set(cell, 'pitch', this.currentSymbol?.pitch);
              this.$set(cell, 'pitchIndex', this.currentSymbol?.pitchIndex);
              this.$set(cell, 'octave', octave);
            } else if (this.cursor.eighthPart === 'sub') {
              this.$set(cell, 'subNote', e.key);
              this.$set(cell, 'subDotUp', dotUp);
              this.$set(cell, 'subDotDown', dotDown);
              this.$set(cell, 'subPitch', this.currentSymbol?.pitch);
              this.$set(cell, 'subPitchIndex', this.currentSymbol?.pitchIndex);
              this.$set(cell, 'subOctave', octave);
            } else if (this.eighthEditTarget === 'main') {
              this.$set(cell, 'note', e.key);
              this.$set(cell, 'dotUp', dotUp);
              this.$set(cell, 'dotDown', dotDown);
              this.$set(cell, 'pitch', this.currentSymbol?.pitch);
              this.$set(cell, 'pitchIndex', this.currentSymbol?.pitchIndex);
              this.$set(cell, 'octave', octave);
            } else if (this.eighthEditTarget === 'sub') {
              this.$set(cell, 'subNote', e.key);
              this.$set(cell, 'subDotUp', dotUp);
              this.$set(cell, 'subDotDown', dotDown);
              this.$set(cell, 'subPitch', this.currentSymbol?.pitch);
              this.$set(cell, 'subPitchIndex', this.currentSymbol?.pitchIndex);
              this.$set(cell, 'subOctave', octave);
            } else {
              // 默认行为：点击左半部分为主音，右半部分为次音
              if (c.clickX < 12) {
                this.$set(cell, 'note', e.key);
                this.$set(cell, 'dotUp', dotUp);
                this.$set(cell, 'dotDown', dotDown);
                this.$set(cell, 'pitch', this.currentSymbol?.pitch);
                this.$set(cell, 'pitchIndex', this.currentSymbol?.pitchIndex);
                this.$set(cell, 'octave', octave);
              } else {
                this.$set(cell, 'subNote', e.key);
                this.$set(cell, 'subDotUp', dotUp);
                this.$set(cell, 'subDotDown', dotDown);
                this.$set(cell, 'subPitch', this.currentSymbol?.pitch);
                this.$set(cell, 'subPitchIndex', this.currentSymbol?.pitchIndex);
                this.$set(cell, 'subOctave', octave);
              }
            }
          }
        } else {
          // 其他音符类型的处理保持不变
          this.$set(cell, 'note', e.key);
          this.$set(cell, 'duration', this.getDurationValue(duration));
          this.$set(cell, 'isWhole', isWhole);
          this.$set(cell, 'dotUp', dotUp);
          this.$set(cell, 'dotDown', dotDown);
          this.$set(cell, 'pitch', this.currentSymbol?.pitch);
          this.$set(cell, 'pitchIndex', this.currentSymbol?.pitchIndex);
          this.$set(cell, 'octave', octave);
          this.$set(cell, 'isEighth', false);
          this.$set(cell, 'subNote', '');
          this.$set(cell, 'subDotUp', 0);
          this.$set(cell, 'subDotDown', 0);
          this.$set(cell, 'subPitch', '');
          this.$set(cell, 'subPitchIndex', 0);
          this.$set(cell, 'subOctave', 4);
          
          if (isWhole) {
            // 填充剩余格子为休止符
            for (let i = c.noteIdx + 1; i < this.noteCount; i++) {
              this.$set(measure.notes[c.staffIdx], i, { 
                note: '-', 
                dotUp: false, 
                dotDown: false, 
                line: false, 
                isRest: true,
                duration: this.getDurationValue(duration)
              });
            }
            
            // 检查是否需要新小节
            const isLastMeasure = (c.rowIdx * this.measuresPerRow + c.mIdx === this.localMeasures.length - 1);
            if (isLastMeasure) {
              const staffCount = this.isDualStaff ? 2 : 1;
              const newNotes = Array.from({ length: staffCount }, () =>
                Array(this.noteCount).fill().map(() => ({ note: '', dotUp: false, dotDown: false, line: false }))
              );
              this.localMeasures.push({
                index: this.localMeasures.length + 1,
                notes: newNotes
              });
            }
            
            // 跳到下一小节
            if (c.mIdx < this.measuresPerRow - 1) {
              this.setCursor(c.rowIdx, c.mIdx + 1, c.staffIdx, 0);
            } else if (c.rowIdx < this.rows.length - 1) {
              this.setCursor(c.rowIdx + 1, 0, c.staffIdx, 0);
            }
          } else if (isHalf) {
            // 二分音符时，在下一个格子添加"-"
            if (c.noteIdx + 1 < this.noteCount) {
              this.$set(measure.notes[c.staffIdx], c.noteIdx + 1, { 
                note: '-', 
                dotUp: false, 
                dotDown: false, 
                line: false, 
                isRest: true,
                duration: this.getDurationValue(duration)
              });
              
              // 移动光标到"-"后面的位置
              if (c.noteIdx + 2 < this.noteCount) {
                this.setCursor(c.rowIdx, c.mIdx, c.staffIdx, c.noteIdx + 2);
              } else if (c.mIdx < this.measuresPerRow - 1) {
                this.setCursor(c.rowIdx, c.mIdx + 1, c.staffIdx, 0);
              } else if (c.rowIdx < this.rows.length - 1) {
                this.setCursor(c.rowIdx + 1, 0, c.staffIdx, 0);
              }
            }

            // 检查是否需要新小节（考虑横杆占用的位置）
            const isLastMeasure = (c.rowIdx * this.measuresPerRow + c.mIdx === this.localMeasures.length - 1);
            const isLastCell = (c.noteIdx + 1 === this.noteCount - 1); // 修改判断条件，考虑横杆占用的位置
            if (isLastMeasure && isLastCell) {
              const staffCount = this.isDualStaff ? 2 : 1;
              const newNotes = Array.from({ length: staffCount }, () =>
                Array(this.noteCount).fill().map(() => ({ note: '', dotUp: false, dotDown: false, line: false }))
              );
              this.localMeasures.push({
                index: this.localMeasures.length + 1,
                notes: newNotes
              });
              // 跳到新小节第一个格子
              this.setCursor(
                Math.floor(this.localMeasures.length / this.measuresPerRow),
                (this.localMeasures.length - 1) % this.measuresPerRow,
                c.staffIdx,
                0
              );
            }
          } else if (isThird) {
            // 三分音符时，在后面的两个格子添加"-"
            if (c.noteIdx + 2 < this.noteCount) {
              // 添加第一个"-"
              this.$set(measure.notes[c.staffIdx], c.noteIdx + 1, { 
                note: '-', 
                dotUp: false, 
                dotDown: false, 
                line: false, 
                isRest: true,
                duration: this.getDurationValue(duration)
              });
              // 添加第二个"-"
              this.$set(measure.notes[c.staffIdx], c.noteIdx + 2, { 
                note: '-', 
                dotUp: false, 
                dotDown: false, 
                line: false, 
                isRest: true,
                duration: this.getDurationValue(duration)
              });
              
              // 移动光标到第二个"-"后面的位置
              if (c.noteIdx + 3 < this.noteCount) {
                this.setCursor(c.rowIdx, c.mIdx, c.staffIdx, c.noteIdx + 3);
              } else if (c.mIdx < this.measuresPerRow - 1) {
                this.setCursor(c.rowIdx, c.mIdx + 1, c.staffIdx, 0);
              } else if (c.rowIdx < this.rows.length - 1) {
                this.setCursor(c.rowIdx + 1, 0, c.staffIdx, 0);
              }
            }

            // 检查是否需要新小节（考虑两个横杆占用的位置）
            const isLastMeasure = (c.rowIdx * this.measuresPerRow + c.mIdx === this.localMeasures.length - 1);
            const isLastCell = (c.noteIdx + 2 === this.noteCount - 1); // 修改判断条件，考虑两个横杆占用的位置
            if (isLastMeasure && isLastCell) {
              const staffCount = this.isDualStaff ? 2 : 1;
              const newNotes = Array.from({ length: staffCount }, () =>
                Array(this.noteCount).fill().map(() => ({ note: '', dotUp: false, dotDown: false, line: false }))
              );
              this.localMeasures.push({
                index: this.localMeasures.length + 1,
                notes: newNotes
              });
              // 跳到新小节第一个格子
              this.setCursor(
                Math.floor(this.localMeasures.length / this.measuresPerRow),
                (this.localMeasures.length - 1) % this.measuresPerRow,
                c.staffIdx,
                0
              );
            }
          } else {
            // 普通音符（四分音符）录入后自动右移光标
            if (c.noteIdx < this.noteCount - 1) {
              this.setCursor(c.rowIdx, c.mIdx, c.staffIdx, c.noteIdx + 1);
            } else if (c.mIdx < this.measuresPerRow - 1) {
              this.setCursor(c.rowIdx, c.mIdx + 1, c.staffIdx, 0);
            } else if (c.rowIdx < this.rows.length - 1) {
              this.setCursor(c.rowIdx + 1, 0, c.staffIdx, 0);
            }
            
            // 检查是否需要新小节
            const isLastMeasure = (c.rowIdx * this.measuresPerRow + c.mIdx === this.localMeasures.length - 1);
            const isLastCell = (c.noteIdx === this.noteCount - 1);
            if (isLastMeasure && isLastCell) {
              const staffCount = this.isDualStaff ? 2 : 1;
              const newNotes = Array.from({ length: staffCount }, () =>
                Array(this.noteCount).fill().map(() => ({ note: '', dotUp: false, dotDown: false, line: false }))
              );
              this.localMeasures.push({
                index: this.localMeasures.length + 1,
                notes: newNotes
              });
              // 跳到新小节第一个格子
              this.setCursor(
                Math.floor(this.localMeasures.length / this.measuresPerRow),
                (this.localMeasures.length - 1) % this.measuresPerRow,
                c.staffIdx,
                0
              );
            }
          }
        }
      } else if (e.key === 'Backspace' || e.key === 'Delete') {
        // 如果是全音符，需要清除所有相关格子
        if (cell.isWhole) {
          for (let i = c.noteIdx; i < this.noteCount; i++) {
            this.$set(measure.notes[c.staffIdx], i, { 
              note: '', 
              dotUp: false, 
              dotDown: false, 
              line: false, 
              isRest: false 
            });
          }
        } else if (cell.isEighth) {
          this.$set(cell, 'note', '');
          this.$set(cell, 'subNote', '');
          this.$set(cell, 'isEighth', false);
          this.$set(cell, 'dotUp', 0);
          this.$set(cell, 'dotDown', 0);
          this.$set(cell, 'subDotUp', 0);
          this.$set(cell, 'subDotDown', 0);
          this.$set(cell, 'pitch', '');
          this.$set(cell, 'subPitch', '');
          this.$set(cell, 'octave', 4);
          this.$set(cell, 'subOctave', 4);
          this.$set(cell, 'line', false);
        } else {
          this.$set(cell, 'note', '');
        }
      } else if (e.key === 'ArrowRight') {
        if (c.noteIdx < this.noteCount - 1) {
          this.setCursor(c.rowIdx, c.mIdx, c.staffIdx, c.noteIdx + 1);
        } else if (c.staffIdx === 0 && this.isDualStaff) {
          this.setCursor(c.rowIdx, c.mIdx, 1, 0);
        } else if (c.mIdx < this.measuresPerRow - 1) {
          this.setCursor(c.rowIdx, c.mIdx + 1, 0, 0);
        } else if (c.rowIdx < this.rows.length - 1) {
          this.setCursor(c.rowIdx + 1, 0, 0, 0);
        }
      } else if (e.key === 'ArrowLeft') {
        if (c.noteIdx > 0) {
          this.setCursor(c.rowIdx, c.mIdx, c.staffIdx, c.noteIdx - 1);
        } else if (c.staffIdx === 1 && this.isDualStaff) {
          this.setCursor(c.rowIdx, c.mIdx, 0, this.noteCount - 1);
        } else if (c.mIdx > 0) {
          this.setCursor(c.rowIdx, c.mIdx - 1, this.isDualStaff ? 1 : 0, this.noteCount - 1);
        } else if (c.rowIdx > 0) {
          this.setCursor(c.rowIdx - 1, this.measuresPerRow - 1, this.isDualStaff ? 1 : 0, this.noteCount - 1);
        }
      } else if (e.key === 'ArrowUp') {
        if (c.staffIdx === 1 && this.isDualStaff) {
          this.setCursor(c.rowIdx, c.mIdx, 0, c.noteIdx);
        }
      } else if (e.key === 'ArrowDown') {
        if (c.staffIdx === 0 && this.isDualStaff) {
          this.setCursor(c.rowIdx, c.mIdx, 1, c.noteIdx);
        }
      }
    },
    onBarlineClick(type, rowIdx, mIdx) {
      // 预留：后续可通过工具栏改变小节线/终止线样式
      // type: 'bar' | 'end'
      // rowIdx, mIdx 定位
      // console.log('barline click', type, rowIdx, mIdx);
    },
    startStaffDrag(e) {
      this.dragging = { type: 'staff', startY: e.clientY, startVal: this.staffSpacing }
      this.draggingActive = true;
      window.addEventListener('mousemove', this.onDragMove)
      window.addEventListener('mouseup', this.onDragEnd)
    },
    startRowDrag(rowIdx, e) {
      this.dragging = { type: 'row', startY: e.clientY, startVal: this.rowHeight, rowIdx }
      this.draggingActive = true;
      window.addEventListener('mousemove', this.onDragMove)
      window.addEventListener('mouseup', this.onDragEnd)
    },
    onDragMove(e) {
      if (!this.dragging) return;
      const delta = e.clientY - this.dragging.startY;
      if (this.dragging.type === 'staff') {
        // 添加合理的限制
        const newSpacing = this.dragging.startVal + delta;
        this.staffSpacing = Math.max(
          this.minStaffSpacing,
          Math.min(newSpacing, this.maxStaffSpacing)
        );
      } else if (this.dragging.type === 'row') {
        const minRowHeight = this.isDualStaff ? this.staffSpacing * 2 + 52 : this.staffSpacing + 52;
        const newHeight = this.dragging.startVal + delta;
        this.rowHeight = Math.max(
          Math.max(minRowHeight, this.minRowHeight),
          Math.min(newHeight, this.maxRowHeight)
        );
      }
    },
    onDragEnd() {
      this.dragging = null
      this.draggingActive = false;
      window.removeEventListener('mousemove', this.onDragMove)
      window.removeEventListener('mouseup', this.onDragEnd)
    },
    isPlayingCell(rowIdx, mIdx, staffIdx, noteIdx) {
      const c = this.playingCursor;
      return c && c.rowIdx === rowIdx && c.mIdx === mIdx && c.staffIdx === staffIdx && c.noteIdx === noteIdx;
    },
    /**
     * 获取时值数值
     */
    getDurationValue(duration) {
      // "全音符"->1, "二分音符"->0.5, "四分音符"->0.25 ...
      if (typeof duration === 'number') return duration;
      if (duration === '全音符') return 1;
      if (duration === '二分音符') return 0.5;
      if (duration === '四分音符') return 0.25;
      if (duration === '八分音符') return 0.125;
      // ...可扩展
      return 0.25;
    },
    async initPiano() {
      if (!this.piano) {
        this.piano = new Tone.Sampler({
          urls: {
            'A0': 'A0.mp3', 'A#0': 'As0.mp3', 'B0': 'B0.mp3',
            'C1': 'C1.mp3', 'C#1': 'Cs1.mp3', 'D1': 'D1.mp3', 'D#1': 'Ds1.mp3', 'E1': 'E1.mp3', 'F1': 'F1.mp3', 'F#1': 'Fs1.mp3', 'G1': 'G1.mp3', 'G#1': 'Gs1.mp3', 'A1': 'A1.mp3', 'A#1': 'As1.mp3', 'B1': 'B1.mp3',
            'C2': 'C2.mp3', 'C#2': 'Cs2.mp3', 'D2': 'D2.mp3', 'D#2': 'Ds2.mp3', 'E2': 'E2.mp3', 'F2': 'F2.mp3', 'F#2': 'Fs2.mp3', 'G2': 'G2.mp3', 'G#2': 'Gs2.mp3', 'A2': 'A2.mp3', 'A#2': 'As2.mp3', 'B2': 'B2.mp3',
            'C3': 'C3.mp3', 'C#3': 'Cs3.mp3', 'D3': 'D3.mp3', 'D#3': 'Ds3.mp3', 'E3': 'E3.mp3', 'F3': 'F3.mp3', 'F#3': 'Fs3.mp3', 'G3': 'G3.mp3', 'G#3': 'Gs3.mp3', 'A3': 'A3.mp3', 'A#3': 'As3.mp3', 'B3': 'B3.mp3',
            'C4': 'C4.mp3', 'C#4': 'Cs4.mp3', 'D4': 'D4.mp3', 'D#4': 'Ds4.mp3', 'E4': 'E4.mp3', 'F4': 'F4.mp3', 'F#4': 'Fs4.mp3', 'G4': 'G4.mp3', 'G#4': 'Gs4.mp3', 'A4': 'A4.mp3', 'A#4': 'As4.mp3', 'B4': 'B4.mp3',
            'C5': 'C5.mp3', 'C#5': 'Cs5.mp3', 'D5': 'D5.mp3', 'D#5': 'Ds5.mp3', 'E5': 'E5.mp3', 'F5': 'F5.mp3', 'F#5': 'Fs5.mp3', 'G5': 'G5.mp3', 'G#5': 'Gs5.mp3', 'A5': 'A5.mp3', 'A#5': 'As5.mp3', 'B5': 'B5.mp3',
            'C6': 'C6.mp3', 'C#6': 'Cs6.mp3', 'D6': 'D6.mp3', 'D#6': 'Ds6.mp3', 'E6': 'E6.mp3', 'F6': 'F6.mp3', 'F#6': 'Fs6.mp3', 'G6': 'G6.mp3', 'G#6': 'Gs6.mp3', 'A6': 'A6.mp3', 'A#6': 'As6.mp3', 'B6': 'B6.mp3',
            'C7': 'C7.mp3', 'C#7': 'Cs7.mp3', 'D7': 'D7.mp3', 'D#7': 'Ds7.mp3', 'E7': 'E7.mp3', 'F7': 'F7.mp3', 'F#7': 'Fs7.mp3', 'G7': 'G7.mp3', 'G#7': 'Gs7.mp3', 'A7': 'A7.mp3', 'A#7': 'As7.mp3', 'B7': 'B7.mp3',
            'C8': 'C8.mp3'
          },
          release: 1,
          baseUrl: "/audio/salamander/"
        }).toDestination();
      }
      this.showLoading = true;
      await this.piano.loaded;
      this.pianoLoaded = true;
      this.showLoading = false;
    },
    /**
     * 播放时，遇到全音符/占位符，合并为一个全音符的时值
     */
    async playScore() {
      try {
        this.stopRequested = false;
        if (this.isPlaying) return;
        this.isPlaying = true;
        if (!this.pianoLoaded) {
          await this.initPiano();
        }
        await Tone.start();
        
        // 获取速度值，确保是有效的数字
        const tempo = parseInt(this.scoreDraft.tempo) || 120;
        // 计算每拍的持续时间（秒）
        const beatDuration = 60 / tempo;
        console.log('播放速度:', tempo, 'BPM, 每拍持续时间:', beatDuration, '秒');

        // 1. 组织每一拍的所有音
        let beats = [];
        for (let rowIdx = 0; rowIdx < this.rows.length; rowIdx++) {
          const row = this.rows[rowIdx];
          for (let mIdx = 0; mIdx < row.length; mIdx++) {
            const measure = row[mIdx];
            for (let noteIdx = 0; noteIdx < this.noteCount; noteIdx++) {
              let notes = [];
              for (let staffIdx = 0; staffIdx < (this.isDualStaff ? 2 : 1); staffIdx++) {
                const cell = measure.notes[staffIdx][noteIdx];
                if (cell && cell.note && cell.note !== '-' && !cell.isRest) {
                  // 检查是否八分音符
                  if (cell.isEighth) {
                    // 添加主音符
                    notes.push({
                      rowIdx, mIdx, staffIdx, noteIdx,
                      note: cell.note,
                      duration: this.getDurationValue(cell.duration) / 2,
                      isEighth: true,
                      isMain: true
                    });
                    // 添加次音符（如果存在）
                    if (cell.subNote) {
                      notes.push({
                        rowIdx, mIdx, staffIdx, noteIdx,
                        note: cell.subNote,
                        duration: this.getDurationValue(cell.duration) / 2,
                        isEighth: true,
                        isMain: false
                      });
                    }
                  } else {
                    // 其他音符类型
                    notes.push({
                      rowIdx, mIdx, staffIdx, noteIdx,
                      note: cell.note,
                      duration: this.getDurationValue(cell.duration)
                    });
                  }
                }
              }
              beats.push(notes);
            }
          }
        }

        // 2. 找到开始播放的位置
        let startIndex = 0;
        if (this.cursor) {
          // 计算当前光标位置对应的拍子索引
          const { rowIdx, mIdx, noteIdx } = this.cursor;
          startIndex = (rowIdx * this.measuresPerRow + mIdx) * this.noteCount + noteIdx;
        }

        // 3. 从指定位置开始播放
        for (let i = startIndex; i < beats.length; i++) {
          if (this.stopRequested) {
            this.isPlaying = false;
            return;
          }

          const notes = beats[i];
          if (notes.length > 0) {
            // 更新播放光标位置
            this.playingCursor = notes[0];
            
            // 计算当前播放音符的位置并滚动到可见区域
            this.scrollToPlayingNote(notes[0]);
            
            // 播放音符
            for (const note of notes) {
              if (note.isEighth) {
                // 八分音符需要等待前一个音符播放完成
                if (note.isMain) {
                  this.playNote(note.note, note.staffIdx);
                } else {
                  // 次音符需要等待主音符播放一半时间
                  setTimeout(() => {
                    this.playNote(note.note, note.staffIdx);
                  }, (beatDuration * 1000) / 2);
                }
              } else {
                this.playNote(note.note, note.staffIdx);
              }
            }
          }
          
          // 等待一拍的时间
          await new Promise(resolve => setTimeout(resolve, beatDuration * 1000));
        }
        
        this.isPlaying = false;
        this.playingCursor = null;
      } catch (error) {
        console.error('播放出错:', error);
        this.isPlaying = false;
        this.playingCursor = null;
      }
    },
    // 添加新方法：滚动到当前播放的音符位置
    scrollToPlayingNote(note) {
      if (!note || !this.$el) return;
      
      const { rowIdx, mIdx } = note;
      const row = this.rows[rowIdx];
      if (!row) return;
      
      const measure = row[mIdx];
      if (!measure) return;
      
      // 计算目标位置
      const targetY = this.topPadding + rowIdx * this.rowHeight;
      const containerHeight = this.$el.clientHeight;
      const scrollTop = this.$el.scrollTop;
      
      // 如果目标位置不在可视区域内，则滚动
      if (targetY < scrollTop || targetY > scrollTop + containerHeight) {
        // 计算滚动位置，使目标位置在容器中间
        const newScrollTop = targetY - (containerHeight / 2);
        this.$el.scrollTo({
          top: newScrollTop,
          behavior: 'smooth'
        });
      }
    },
    playNote(note, staffIdx) {
      if (!this.piano) return;
      const cell = this.getCell(this.rows[this.playingCursor.rowIdx][this.playingCursor.mIdx], staffIdx, this.playingCursor.noteIdx);
      const noteName = this.jianpuToNoteName(note, cell);
      this.piano.triggerAttackRelease(noteName, this.getDurationValue(this.rows[this.playingCursor.rowIdx][this.playingCursor.mIdx].duration));
      
      // 触发 notePlayed 事件
      this.$emit('notePlayed', note, cell);
    },
    
    jianpuToNoteName(note, cell) {
      // 1=C, 2=D, 3=E, 4=F, 5=G, 6=A, 7=B
      const map = { '1': 'C', '2': 'D', '3': 'E', '4': 'F', '5': 'G', '6': 'A', '7': 'B' };
      let octave = 4;
      if (cell) {
        if (cell.pitch === '小字五组') octave = 8;
        else if (cell.pitch === '小字四组') octave = 7;
        else if (cell.pitch === '小字三组') octave = 6;
        else if (cell.pitch === '小字二组') octave = 5;
        else if (typeof cell.octave === 'number') octave = cell.octave;
      }
      return map[note] + octave;
    },
    
    stopScore() {
      this.stopRequested = true;
    },
    playMeasure(measure, staffIndex) {
      const bpm = this.bpm;
      const beatDuration = 60 / bpm;
      
      measure.notes[staffIndex].forEach(cell => {
        if (cell.note) {
          if (cell.isEighth) {
            // 播放八分音符的主音符
            this.playNote(cell.note, beatDuration * 0.5, cell.octave);
            // 播放八分音符的次音符
            if (cell.subNote) {
              setTimeout(() => {
                this.playNote(cell.subNote, beatDuration * 0.5, cell.octave);
              }, beatDuration * 0.5 * 1000);
            }
          } else {
            // 其他音符类型的播放保持不变
            this.playNote(cell.note, beatDuration * cell.duration, cell.octave);
          }
        }
      });
    },
    saveScore() {
      const data = JSON.stringify(this.localMeasures, null, 2);
      const blob = new Blob([data], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = 'jianpu-score.json';
      a.click();
      URL.revokeObjectURL(url);
    },
    openScore() {
      const input = document.createElement('input');
      input.type = 'file';
      input.accept = '.json,application/json';
      input.onchange = async e => {
        const file = e.target.files[0];
        if (!file) return;
        
        // 检查文件大小
        if (file.size > 1024 * 1024) { // 1MB
          this.$message.error('文件太大，请选择小于1MB的文件');
          return;
        }
        
        try {
          const text = await file.text();
          let data;
          try {
            data = JSON.parse(text);
          } catch (err) {
            throw new Error('文件格式不正确，请确保是有效的JSON文件');
          }
          
          // 验证数据格式
          if (!Array.isArray(data)) {
            throw new Error('文件格式不正确，数据必须是数组');
          }

          // 验证每个小节的数据结构
          if (!this.validateScoreData(data)) {
            throw new Error('文件格式不正确，请确保数据包含必要的小节信息');
          }
          
          // 重置撤销/重做状态
          this.resetUndoRedo();
          
          // 确保每个小节都有正确的notes数组结构
          data = data.map(measure => {
            if (!measure.notes) {
              const staffCount = this.isDualStaff ? 2 : 1;
              measure.notes = Array.from({ length: staffCount }, () =>
                Array(this.noteCount).fill().map(() => ({ note: '', dotUp: false, dotDown: false, line: false }))
              );
            }
            return measure;
          });
          
          // 更新数据
          this.localMeasures = data;
          
          // 保存初始状态
          this.pushUndo();

          // 显示成功消息
          this.$message.success('文件加载成功');
        } catch (err) {
          console.error('文件加载失败:', err);
          this.$message.error(err.message || '文件加载失败，请重试');
        }
      };
      input.click();
    },
    validateScoreData(data) {
      // 验证数据格式
      return data.every(measure => {
        // 检查基本属性
        if (!measure || typeof measure !== 'object') return false;
        if (!measure.index || typeof measure.index !== 'number') return false;
        
        // 检查notes数组
        if (!Array.isArray(measure.notes)) return false;
        
        // 检查每个声部
        return measure.notes.every(staff => {
          if (!Array.isArray(staff)) return false;
          
          // 检查每个音符
          return staff.every(note => {
            if (!note || typeof note !== 'object') return false;
            if (!('note' in note)) return false;
            return true;
          });
        });
      });
    },
    resetUndoRedo() {
      this.undoStack = [];
      this.redoStack = [];
      this.isUndoRedoOperation = false;
    },
    pushUndo() {
      // 如果是撤销/重做操作，不记录新的撤销状态
      if (this.isUndoRedoOperation) {
        this.isUndoRedoOperation = false;
        return;
      }
      
      // 深拷贝当前状态
      const currentState = JSON.parse(JSON.stringify(this.localMeasures));
      this.undoStack.push(currentState);
      
      // 限制撤销栈大小
      if (this.undoStack.length > this.maxUndoStackSize) {
        this.undoStack.shift(); // 移除最旧的记录
      }
      
      // 清空重做栈
      this.redoStack = [];
    },
    undo() {
      if (this.undoStack.length === 0) return;
      
      // 标记这是一个撤销/重做操作
      this.isUndoRedoOperation = true;
      
      // 保存当前状态到重做栈
      const currentState = JSON.parse(JSON.stringify(this.localMeasures));
      this.redoStack.push(currentState);
      
      // 恢复上一个状态
      const previousState = this.undoStack.pop();
      
      // 使用Vue的响应式方法更新状态
      this.localMeasures = [];
      this.$nextTick(() => {
        this.localMeasures = previousState;
        this.$forceUpdate();
      });
    },
    redo() {
      if (this.redoStack.length === 0) return;
      
      // 标记这是一个撤销/重做操作
      this.isUndoRedoOperation = true;
      
      // 保存当前状态到撤销栈
      const currentState = JSON.parse(JSON.stringify(this.localMeasures));
      this.undoStack.push(currentState);
      
      // 恢复下一个状态
      const nextState = this.redoStack.pop();
      
      // 使用Vue的响应式方法更新状态
      this.localMeasures = [];
      this.$nextTick(() => {
        this.localMeasures = nextState;
        this.$forceUpdate();
      });
    },
    selectMeasure(rowIdx, mIdx) {
      this.selectedMeasure = { rowIdx, mIdx };
      this.hideContextMenu();
    },
    isSelectedMeasure(rowIdx, mIdx) {
      return this.selectedMeasure &&
        this.selectedMeasure.rowIdx === rowIdx &&
        this.selectedMeasure.mIdx === mIdx;
    },
    async confirmDeleteMeasure() {
      // 只要有选中小节就允许删除
      const selected = this.selectedMeasure;
      if (!selected) return;
      const flatIdx = selected.rowIdx * this.measuresPerRow + selected.mIdx;
      if (flatIdx < 0 || flatIdx >= this.localMeasures.length) return;
      try {
        await this.$confirm('确定要删除当前小节吗？', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        });
      } catch {
        return;
      }
      this.localMeasures.splice(flatIdx, 1);
      // 重新编号
      this.localMeasures.forEach((m, i) => { m.index = i + 1; });
      // 重新排布高亮
      let newIdx = flatIdx;
      if (newIdx >= this.localMeasures.length) newIdx = this.localMeasures.length - 1;
      if (newIdx < 0) {
        this.selectedMeasure = null;
      } else {
        const rowIdx = Math.floor(newIdx / this.measuresPerRow);
        const mIdx = newIdx % this.measuresPerRow;
        this.selectedMeasure = { rowIdx, mIdx };
      }
    },
    clearSelectedMeasure() {
      this.selectedMeasure = null;
      this.hideContextMenu();
    },
    onContextMenu(e, rowIdx, mIdx) {
      if (!this.isSelectedMeasure(rowIdx, mIdx)) return;
      e.preventDefault();
      // 让菜单出现在鼠标点右侧偏移20px
      const menuWidth = 160, menuHeight = 38;
      let x = e.clientX + 20;
      let y = e.clientY;
      const docW = document.documentElement.clientWidth;
      const docH = document.documentElement.clientHeight;
      if (x + menuWidth > docW) x = docW - menuWidth - 2;
      if (y + menuHeight > docH) y = docH - menuHeight - 2;
      this.showContextMenu = true;
      this.contextMenuX = x;
      this.contextMenuY = y;
    },
    hideContextMenu() {
      this.showContextMenu = false;
    },
    onDeleteMeasureMenu() {
      this.hideContextMenu();
      // 右键菜单直接用当前选中的小节
      this.confirmDeleteMeasure();
    },
    onAddLyricMenu() {
      this.hideContextMenu();
      const selected = this.selectedMeasure;
      if (!selected) return;
      const measure = this.rows[selected.rowIdx]?.[selected.mIdx];
      if (measure) {
        this.$set(measure, 'lyricEditing', true);
        this.$set(measure, 'lyricDraft', measure.lyric || '');
        this.$nextTick(() => {
          // 自动聚焦
          const input = this.$refs.lyricInput;
          if (input && input.focus) input.focus();
        });
      }
    },
    onInsertMeasureMenu() {
      this.hideContextMenu();
      const selected = this.selectedMeasure;
      if (!selected) return;
      const flatIdx = selected.rowIdx * this.measuresPerRow + selected.mIdx;
      if (flatIdx < 0 || flatIdx >= this.localMeasures.length) return;
      const staffCount = this.isDualStaff ? 2 : 1;
      const newNotes = Array.from({ length: staffCount }, () =>
        Array(this.noteCount).fill().map(() => ({ note: '', dotUp: false, dotDown: false, line: false }))
      );
      this.localMeasures.splice(flatIdx + 1, 0, {
        index: 0, // 稍后统一编号
        notes: newNotes
      });
      // 重新编号
      this.localMeasures.forEach((m, i) => { m.index = i + 1; });
      // 选中新插入的小节
      const newIdx = flatIdx + 1;
      const rowIdx = Math.floor(newIdx / this.measuresPerRow);
      const mIdx = newIdx % this.measuresPerRow;
      this.selectedMeasure = { rowIdx, mIdx };
    },
    onLyricInputBlur(measure, rowIdx, mIdx) {
      this.saveLyric(measure, rowIdx, mIdx);
    },
    onLyricInputEnter(measure, rowIdx, mIdx) {
      this.saveLyric(measure, rowIdx, mIdx);
    },
    saveLyric(measure, rowIdx, mIdx) {
      if (!measure) return;
      this.$set(measure, 'lyric', measure.lyricDraft || '');
      this.$set(measure, 'lyricEditing', false);
    },
    onLyricTextClick(measure) {
      this.$set(measure, 'lyricEditing', true);
      this.$set(measure, 'lyricDraft', measure.lyric || '');
      this.$nextTick(() => {
        const input = this.$refs.lyricInput;
        if (input && input.focus) input.focus();
      });
    },
    getLyricY() {
      // 双声部歌词在下方，单声部歌词靠近音符
      return this.isDualStaff
        ? this.noteBaseY - 18 + this.staffSpacing + 30 + 28
        : this.noteBaseY - 18 + 30 + 28;
    },
  },
  mounted() {
    this.initPiano();
    // 初始化localMeasures根据noteCount和声部
    if (!this.localMeasures.length) {
      const staffCount = this.isDualStaff ? 2 : 1;
      for (let i = 0; i < 4; i++) {
        const notes = Array.from({ length: staffCount }, () =>
          Array(this.noteCount).fill().map(() => ({ note: '', dotUp: false, dotDown: false, line: false }))
        );
        this.localMeasures.push({ index: i + 1, notes });
      }
    }
    // 监听chord名变更事件
    this.$on && this.$on('update:chord', this.applyChordToCurrentCell);
    // 禁用浏览器右键
    this.$el.addEventListener('contextmenu', e => e.preventDefault());
    // 全局点击关闭菜单
    window.addEventListener('mousedown', this.hideContextMenu);
    // 修正 rowHeight，保证初始手柄和高亮框位置正确
    const minRowHeight = this.isDualStaff ? this.staffSpacing * 2 + 52 : this.staffSpacing + 52;
    if (this.rowHeight < minRowHeight) {
      this.rowHeight = minRowHeight;
    }
    // 保存初始状态到撤销栈
    this.pushUndo();
  },
  watch: {
    currentChord(newChord) {
      if (newChord) {
        this.applyChordToCurrentCell(newChord)
      }
    }
  },
  beforeDestroy() {
    // 清理所有事件监听器
    window.removeEventListener('mousedown', this.hideContextMenu);
    this.$el.removeEventListener('contextmenu', e => e.preventDefault());
    this.$off && this.$off('update:chord', this.applyChordToCurrentCell);
    
    // 停止播放
    this.stopScore && this.stopScore();
    
    // 清理音频资源
    if (this.piano) {
      this.piano.dispose();
      this.piano = null;
    }
  },
}
</script>
<style scoped>
.jianpu-scroll-container {
  overflow-y: auto;
  max-height: calc(100vh - 220px);
}
.jianpu-editor-svg {
  width: 100%;
  min-height: 120px;
  border-radius: 8px;
  box-sizing: border-box;
  overflow-x: auto;
  outline: none;
}
.jianpu-loading-mask {
  position: absolute;
  z-index: 1000;
  left: 0; top: 0; right: 0; bottom: 0;
  background: rgba(255,255,255,0.7);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.jianpu-loading-spinner {
  border: 4px solid #f3f3f3;
  border-top: 4px solid #409eff;
  border-radius: 50%;
  width: 48px;
  height: 48px;
  animation: spin 1s linear infinite;
  margin-bottom: 16px;
}
@keyframes spin {
  0% { transform: rotate(0deg);}
  100% { transform: rotate(360deg);}
}
.jianpu-loading-text {
  color: #409eff;
  font-size: 16px;
  font-weight: bold;
}
.selected-measure rect {
  pointer-events: none;
}
.measure-group.selected-measure {
  filter: drop-shadow(0 0 6px #ff9800aa);
}
.jianpu-context-menu {
  position: fixed;
  z-index: 2000;
  background: #fff;
  border: 1px solid #bdbdbd;
  border-radius: 3px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.18);
  min-width: 160px;
  padding: 4px 0;
  font-size: 15px;
  color: #222;
  user-select: none;
  outline: none;
  animation: contextMenuFadeIn 0.13s;
}
@keyframes contextMenuFadeIn {
  from { opacity: 0; transform: scale(0.98); }
  to { opacity: 1; transform: scale(1); }
}
.jianpu-context-menu-item {
  padding: 6px 32px 6px 32px;
  cursor: pointer;
  color: #222;
  white-space: nowrap;
  position: relative;
  transition: background 0.15s;
}
.jianpu-context-menu-item:hover {
  background: #e8f0fe;
  color: #111;
}
.jianpu-context-menu-item:active {
  background: #c6dafc;
}
/* 歌词inline输入样式 */
.jianpu-lyric-inline-input {
  width: 100%;
  text-align: left;
  font-size: 12px;
  color: #ff9800;
  border: 1.5px solid #ff9800;
  border-radius: 4px;
  padding: 2px 6px;
  outline: none;
  background: #fff7e6;
  box-sizing: border-box;
}
.jianpu-lyric-inline-text {
  width: 100%;
  text-align: left;
  font-size: 12px;
  color: #ff9800;
  user-select: none;
  cursor: pointer;
  min-height: 18px;
  line-height: 18px;
  background: transparent;
}
</style>